/******************************************************************************

ADL SCORM 2004 4th Edition Sample Run-Time Environment

The ADL SCORM 2004 4th Ed. Sample Run-Time Environment is licensed under
Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States.

The Advanced Distributed Learning Initiative allows you to:
  *  Share - to copy, distribute and transmit the work.
  *  Remix - to adapt the work. 

Under the following conditions:
  *  Attribution. You must attribute the work in the manner specified by the author or
     licensor (but not in any way that suggests that they endorse you or your use
     of the work).
  *  Noncommercial. You may not use this work for commercial purposes. 
  *  Share Alike. If you alter, transform, or build upon this work, you may distribute
     the resulting work only under the same or similar license to this one. 

For any reuse or distribution, you must make clear to others the license terms of this work. 

Any of the above conditions can be waived if you get permission from the ADL Initiative. 
Nothing in this license impairs or restricts the author's moral rights.

*******************************************************************************/

package org.adl.datamodels.datatypes;

import org.adl.datamodels.DMTypeValidator;
import org.adl.datamodels.DMErrorCodes;
import java.io.Serializable;
import java.util.Vector;
import java.util.GregorianCalendar;
import java.util.Calendar;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

/**
 * <br><br>
 * 
 * <strong>Filename:</strong> DateTimeValidator.java<br><br>
 * 
 * <strong>Description:</strong><br><br>
 * 
 * <strong>Design Issues:</strong><br><br>
 * 
 * <strong>Implementation Issues:</strong><br><br>
 * 
 * <strong>Known Problems:</strong><br><br>
 * 
 * <strong>Side Effects:</strong><br><br>
 * 
 * <strong>References:</strong><br>
 * <ul>
 *     <li>SCORM 2004
 * </ul>
 * 
 * @author ADL Technical Team
 */
public class DateTimeValidator extends DMTypeValidator implements Serializable
{

   /**
    * A constant holding the second field upper bound: 59
    */
   private static final int SECOND_UPPER_BOUND = 59;

   /**
    * A constant holding the minute field upper bound: 59
    */
   private static final int MIN_UPPER_BOUND = 59;

   /**
    * A constant holding the hour field upper bound: 23
    */
   private static final int HOUR_UPPER_BOUND = 23;

   /**
    * A constant holding the month field upper bound: 11
    */
   private static final int MONTH_UPPER_BOUND = 11;

   /**
    * A constant holding the year field lower bound: 1970 
    */
   private static final int YEAR_LOWER_BOUND = 1970;

   /**
    * A constant holding the year field upper bound: 2038
    */
   private static final int YEAR_UPPER_BOUND = 2038;


   /**
    * Indicates if subseconds should be allowed -- and tested
    */
   private boolean mIncludeSubSecs = true;


   /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
   
    Constructors
   
   -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

   /**
    * Constructor for this type
    * 
    * @param iInclude Indicates if subseconds should be tested for in this 
    * validator.
    */
   public DateTimeValidator(boolean iInclude)
   {
      mIncludeSubSecs = iInclude; 
   }


   /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
   
    Public Methods
   
   -*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

   /**
    * Compares two valid data model elements for equality.
    * 
    * @param iFirst  The first value being compared.
    * 
    * @param iSecond The second value being compared.
    * 
    * @param iDelimiters The common set of delimiters associated with the
    * values being compared.
    * 
    * @return Returns <code>true</code> if the two values are equal, otherwise
    *         <code>false</code>.
    */
   public boolean compare(String iFirst, String iSecond, Vector iDelimiters)
   {

      boolean equal = true;

      DateTimeFormatter dtp = ISODateTimeFormat.dateTimeParser();

      try
      {
         // Parse the first string and remove the sub-seconds
         DateTime dt1 = dtp.parseDateTime(iFirst);
         dt1 = new DateTime(dt1.getYear(), 
                            dt1.getMonthOfYear(),
                            dt1.getDayOfMonth(),
                            dt1.getHourOfDay(),
                            dt1.getMinuteOfHour(),
                            dt1.getSecondOfMinute(),
                            0);

         // Parse the second string and remove the sub-seconds
         DateTime dt2 = dtp.parseDateTime(iSecond);
         dt2 = new DateTime(dt2.getYear(), 
                            dt2.getMonthOfYear(),
                            dt2.getDayOfMonth(),
                            dt2.getHourOfDay(),
                            dt2.getMinuteOfHour(),
                            dt2.getSecondOfMinute(),
                            0);

         equal = dt1.equals(dt2);
      }
      catch (Exception e)
      {
         // String format error -- these cannot be equal
         equal = false;
      }

      return equal;
   }

   /**
    * Validates the provided string against a known format.
    * 
    * @param iValue The value being validated.
    * 
    * @return An abstract data model error code indicating the result of this
    *         operation.
    */
   public int validate(String iValue)
   {
      // Assume the value is valid
      int valid = DMErrorCodes.NO_ERROR;

      int idx = -1;
      boolean done = false;

      int year = 0;
      int month = 0;
      int day = 0;

      int tempLength = 0;

      // Check for Null case
      if ( iValue == null )
      {
         // A null value can never be valid
         valid = DMErrorCodes.UNKNOWN_EXCEPTION;
         done = true;
      }

      if ( !done )
      {
         tempLength = iValue.length();

         // First ensure that the year is present
         if ( tempLength < 4 )
         {
            valid = DMErrorCodes.TYPE_MISMATCH;
            return valid;
         }

         try
         {
            year = Integer.parseInt( iValue.substring(0, 4) );

            if ( year < YEAR_LOWER_BOUND || year > YEAR_UPPER_BOUND )
            {
               valid = DMErrorCodes.TYPE_MISMATCH;
               return valid;
            }
         }
         catch ( Exception e )
         {
            valid = DMErrorCodes.TYPE_MISMATCH;
            done = true;
         }

         if ( !done )
         {
            idx += 5;

            // Make sure there is something else to look at
            if ( idx != iValue.length() )
            {
               // Check for a month
               if ( iValue.charAt(idx) == '-' )
               {
                  idx++;

                  // Found month, test range
                  month = 0;
                  try
                  {
                     month = 
                     Integer.parseInt( iValue.substring(idx, idx + 2) );

                     month -= 1;

                     if ( month < 0 || month > MONTH_UPPER_BOUND )
                     {
                        valid = DMErrorCodes.TYPE_MISMATCH;
                        done = true;
                     }
                  }
                  catch ( Exception e )
                  {
                     valid = DMErrorCodes.TYPE_MISMATCH;
                     done = true;
                  }
               }
               else
               {
                  valid = DMErrorCodes.TYPE_MISMATCH;
                  done = true;
               }
            }
            else
            {
               // NO Error because only the year is valid
               valid = DMErrorCodes.NO_ERROR;
               done = true;
            }
         }

         // Check for day
         if ( !done )
         {
            idx += 2;

            if ( idx != iValue.length() )
            {
               if ( iValue.charAt(idx) == '-' )
               {
                  idx++;

                  // Found day, test range
                  try
                  {
                     day = Integer.parseInt( iValue.substring(idx, idx + 2) );

                     // Create a calendar for the indicated year and month
                     Calendar cal = new GregorianCalendar(year, month, 1);

                     // Get the number of days in that month
                     int days = cal.getActualMaximum(Calendar.DAY_OF_MONTH);

                     if ( day < 1 || day > days )
                     {
                        valid = DMErrorCodes.TYPE_MISMATCH;
                        done = true;
                     }
                     else
                     {
                        // Valid day, but need to check if we are at the end
                        idx += 2;
                        if ( idx == iValue.length() )
                        {
                           done = true;
                        }
                        else
                        {
                           // Make sure the next section is describing a time
                           if ( iValue.charAt(idx) != 'T' )
                           {
                              valid = DMErrorCodes.TYPE_MISMATCH;
                              done = true;
                           }
                        }
                     }
                  }
                  catch ( Exception e )
                  {
                     valid = DMErrorCodes.TYPE_MISMATCH;
                     done = true;
                  }
               }
               else
               {
                  valid = DMErrorCodes.TYPE_MISMATCH;
                  done = true;
               }
            }
         }
      }

      // Look for Time
      if ( !done )
      {
         // Check for a hour
         idx = iValue.indexOf("T");

         if ( idx != -1 )
         {
            idx++;

            // Found hour, test range
            int hour = -1;
            try
            {
               hour = Integer.parseInt( iValue.substring(idx, idx + 2) );

               if ( hour < 0 || hour > HOUR_UPPER_BOUND )
               {
                  valid = DMErrorCodes.TYPE_MISMATCH;
                  done = true;
               }
            }
            catch ( Exception e )
            {
               valid = DMErrorCodes.TYPE_MISMATCH;
               done = true;
            }

            // Check for minutes
            if ( !done )
            {
               idx += 2;

               if ( idx != iValue.length() )
               {
                  if ( iValue.charAt(idx) == ':' )
                  {
                     idx++;

                     // Found minutes, test range
                     int minutes = -1;
                     try
                     {
                        minutes = 
                        Integer.parseInt( iValue.substring(idx, idx + 2) );

                        if ( minutes < 0 || minutes > MIN_UPPER_BOUND )
                        {
                           valid = DMErrorCodes.TYPE_MISMATCH;
                           done = true;
                        }
                     }
                     catch ( Exception e )
                     {
                        valid = DMErrorCodes.TYPE_MISMATCH;
                        done = true;
                     }
                  }
                  else
                  {
                     valid = DMErrorCodes.TYPE_MISMATCH;
                     done = true;
                  }
               }
               else
               {
                  done = true;
               }

               // Check for seconds
               if ( !done )
               {
                  // Move past the minutes
                  idx += 2;

                  if ( idx != iValue.length() )
                  {
                     if ( iValue.charAt(idx) == ':' )
                     {
                        idx++;

                        // Found seconds, test range
                        int seconds = -1;
                        try
                        {
                           seconds =
                           Integer.parseInt( iValue.substring(idx, idx + 2) );

                           if ( seconds < 0 || seconds > SECOND_UPPER_BOUND )
                           {
                              valid = DMErrorCodes.TYPE_MISMATCH;
                              done = true;
                           }
                        }
                        catch ( Exception e )
                        {
                           valid = DMErrorCodes.TYPE_MISMATCH;
                           done = true;
                        }
                     }
                     else
                     {
                        valid = DMErrorCodes.TYPE_MISMATCH;
                        done = true;
                     }
                  }
                  else
                  {
                     done = true;
                  }
               }

               // Move past the seconds
               idx += 2;

               int sub = -1;
               // Check for sub-seconds
               if ( !done )
               {
                  if ( mIncludeSubSecs )
                  {
                     if ( idx != iValue.length() )
                     {
                        if ( iValue.charAt(idx) == '.' )
                        {
                           // Move past the '.'
                           idx++;
                           sub = idx;

                           int lengthCounter = 0;

                           // Found start of subseconds, try to find the end
                           while ( sub < iValue.length() &&
                                   iValue.charAt(sub) != 'Z' &&
                                   iValue.charAt(sub) != 'z' &&
                                   iValue.charAt(sub) != '+' &&
                                   iValue.charAt(sub) != '-' )
                           {
                              sub++;
                              lengthCounter++;
                           }

                           // Check if the length is out of range
                           if ( lengthCounter == 0 || lengthCounter > 2 )
                           {
                              valid = DMErrorCodes.TYPE_MISMATCH;
                              done = true;
                           }
                           else
                           {
                              try
                              {
                                 // Try to parse the integer value, if exception
                                 // is thrown, then set appropriate error code.
                                 Integer.parseInt( iValue.substring(idx, sub) );

                                 // Move up to the TZD
                                 idx = sub;
                              }
                              catch ( Exception e )
                              {
                                 valid = DMErrorCodes.TYPE_MISMATCH;
                                 done = true;
                              }
                           }
                        }
                        else
                        {
                           valid = DMErrorCodes.TYPE_MISMATCH;
                        }
                     }
                  }
               }

               // Check for Time Zone Designator
               if ( !done )
               {
                  // Check if we have anything left to check
                  if ( idx < iValue.length() )
                  {
                     if ( iValue.charAt(idx) == 'Z' ||
                          iValue.charAt(idx) == 'z' )
                     {
                        // This has to be the last character
                        if ( idx != (iValue.length() - 1) )
                        {
                           valid = DMErrorCodes.TYPE_MISMATCH;
                           done = true;
                        }

                        // We're done
                        idx = -1;
                     }
                     else if ( iValue.charAt(idx) == '-'  ||
                               iValue.charAt(idx) == '+' )
                     {
                        idx++;
                     }
                     else
                     {
                        valid = DMErrorCodes.TYPE_MISMATCH;

                        done = true;
                        idx = -1;
                     }

                     if ( idx != -1 )
                     {
                        // Check if an hour is properly defined
                        if ( iValue.length() >= (idx + 2) )
                        {
                           // Found hour, test range
                           hour = -1;
                           try
                           {
                              hour =
                              Integer.
                              parseInt(iValue.substring(idx, idx + 2));

                              if ( hour < 0 ||
                                   hour > HOUR_UPPER_BOUND )
                              {
                                 valid = DMErrorCodes.TYPE_MISMATCH;
                                 done = true;
                              }
                           }
                           catch ( Exception e )
                           {
                              valid = DMErrorCodes.TYPE_MISMATCH;
                              done = true;
                           }
                        }
                        else
                        {
                           valid = DMErrorCodes.TYPE_MISMATCH;
                           done = true;
                        }

                        // Check for minutes
                        if ( !done )
                        {
                           // Move past the minutes
                           idx += 2;

                           if ( idx != iValue.length() )
                           {
                              if ( iValue.charAt(idx) == ':' )
                              {
                                 idx++;

                                 // Found minutes, test range
                                 int minutes = -1;
                                 try
                                 {
                                    minutes =
                                    Integer.
                                    parseInt( iValue.substring(idx, idx + 2) );

                                    // Make sure we are at the end
                                    idx += 2;

                                    if ( minutes < 0 ||
                                         minutes > MIN_UPPER_BOUND ||
                                         idx != iValue.length() )
                                    {
                                       valid = DMErrorCodes.TYPE_MISMATCH;
                                    }
                                 }
                                 catch ( Exception e )
                                 {
                                    valid = DMErrorCodes.TYPE_MISMATCH;
                                 }
                              }
                              else
                              {
                                 valid = DMErrorCodes.TYPE_MISMATCH;
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
      }

      return valid;
   }

} // end DateTimeValidator
